package it.niedermann.android.markdown.markwon.plugins


import android.text.style.ClickableSpan
import android.util.Range
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import io.noties.markwon.base.AbstractMarkwonPlugin
import io.noties.markwon.base.MarkwonVisitor
import io.noties.markwon.base.SpannableBuilder
import io.noties.markwon.base.SpannableBuilder.Companion.setSpans
import io.noties.markwon.plugin.tasklist.model.TaskListItem
import io.noties.markwon.plugin.tasklist.TaskListProps
import io.noties.markwon.plugin.tasklist.span.TaskListSpan
import it.niedermann.android.markdown.MarkdownUtil
import it.niedermann.android.markdown.markwon.span.ToggleTaskListSpan
import org.commonmark.node.*
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.function.BiConsumer
import java.util.stream.Collectors

/**
 * @see [Support from upstream](https://github.com/noties/Markwon/issues/196.issuecomment-751680138)
 *
 * @see [Original kotlin implementation](https://github.com/noties/Markwon/blob/910bf311dac1bade400616a00ab0c9b7b7ade8cb/app-sample/src/main/java/io/noties/markwon/app/samples/tasklist/TaskListMutateNestedSample.kt)
 */
class ToggleableTaskListPlugin(private val toggleListener: BiConsumer<Int, Boolean>) :
    AbstractMarkwonPlugin() {
    private val enabled = AtomicBoolean(true)

    fun setEnabled(enabled: Boolean) {
        this.enabled.set(enabled)
    }

    /**
     * Prepares [TaskListSpan]s and marks each one with a [ToggleMarkerSpan] in the first step.
     * The [ToggleMarkerSpan] are different from [TaskListSpan]s as they will stop on nested tasks instead of spanning the whole tasks including its subtasks.
     */
    override fun configureVisitor(builder: MarkwonVisitor.Builder) {
        builder.on(TaskListItem::class.java){ visitor: MarkwonVisitor, node: TaskListItem ->
            val length = visitor.length()
            visitor.visitChildren(node)
            TaskListProps.DONE[visitor.renderProps()] = node.isDone
            val spanFactory = visitor.configuration().spansFactory()[TaskListItem::class.java]

            spanFactory?.getSpans(visitor.configuration(), visitor.renderProps())?.let { spans->
                val taskListSpan: TaskListSpan? = if (spans is Array<*>) {
                    if ((spans as Array<TaskListSpan>).isNotEmpty()) {
                        spans[0]
                    } else {
                        null
                    }
                } else if (spans is TaskListSpan) {
                    spans
                } else {
                    null
                }
                val content = TaskListContextVisitor.contentLength(node)
                if (content > 0 && taskListSpan != null) {
                    // maybe additionally identify this task list (for persistence)
                    visitor.builder().setSpan(
                        ToggleMarkerSpan(taskListSpan),
                        length,
                        length + content
                    )
                }

                setSpans(
                    visitor.builder(),
                    spans,
                    length,
                    visitor.length()
                )
            }

            if (visitor.hasNext(node)) {
                visitor.ensureNewLine()
            }
        }
    }

    /**
     * Adds for each symbolic [ToggleMarkerSpan] an actual [ToggleTaskListSpan]s respecting existing [ClickableSpan]s.
     */
    override fun afterRender(node: Node, visitor: MarkwonVisitor) {
        super.afterRender(node, visitor)
        val markerSpans = getSortedSpans(
            visitor.builder(),
            ToggleMarkerSpan::class.java,
            0,
            visitor.builder().length
        )
        for (position in markerSpans.indices) {
            val markerSpan = markerSpans[position]
            val start = markerSpan.start
            val end = markerSpan.end
            val freeRanges = findFreeRanges(visitor.builder(), start, end)
            for (freeRange in freeRanges) {
                visitor.builder().setSpan(
                    ToggleTaskListSpan(
                        enabled,
                        toggleListener,
                        (markerSpan.what as ToggleMarkerSpan).taskListSpan,
                        position
                    ),
                    freeRange.lower, freeRange.upper
                )
            }
        }
    }

    /**
     * Removes [ToggleMarkerSpan]s from {@param textView}.
     */
    override fun afterSetText(textView: TextView) {
        super.afterSetText(textView)
        val spannable = MarkdownUtil.getContentAsSpannable(textView)
        for (span in spannable.getSpans(0, spannable.length, ToggleMarkerSpan::class.java)) {
            spannable.removeSpan(span)
        }
        textView.text = spannable
    }

    private class TaskListContextVisitor : AbstractVisitor() {
        private var contentLength = 0
        override fun visit(text: Text) {
            super.visit(text)
            contentLength += text.literal.length
        }

        // NB! if count both soft and hard breaks as having length of 1
        override fun visit(softLineBreak: SoftLineBreak) {
            super.visit(softLineBreak)
            contentLength += 1
        }

        // NB! if count both soft and hard breaks as having length of 1
        override fun visit(hardLineBreak: HardLineBreak) {
            super.visit(hardLineBreak)
            contentLength += 1
        }

        override fun visitChildren(parent: Node) {
            var node = parent.firstChild
            while (node != null) {
                // A subclass of this visitor might modify the node, resulting in getNext returning a different node or no
                // node after visiting it. So get the next node before visiting.
                val next = node.next
                if (node is Block && node !is Paragraph) {
                    break
                }
                node.accept(this)
                node = next
            }
        }

        companion object {
            fun contentLength(node: Node): Int {
                val visitor = TaskListContextVisitor()
                visitor.visitChildren(node)
                return visitor.contentLength
            }
        }
    }

    /**
     * Helper class which holds an [TaskListSpan] but does not include the range of child [TaskListSpan]s.
     */
    @VisibleForTesting
    internal class ToggleMarkerSpan constructor( val taskListSpan: TaskListSpan)
    companion object {
        /**
         * @return a [List] of [Range]s in the given {@param spanned} from {@param start} to {@param end} which is **not** taken for a [ClickableSpan].
         */
        private fun findFreeRanges(
            builder: SpannableBuilder,
            start: Int,
            end: Int
        ): Collection<Range<Int>> {
            val freeRanges: List<Range<Int>>
            val clickableSpans = getSortedSpans(builder, ClickableSpan::class.java, start, end)
            if (clickableSpans.isNotEmpty()) {
                freeRanges = LinkedList()
                var from = start
                for (clickableSpan in clickableSpans) {
                    val clickableStart = clickableSpan.start
                    val clickableEnd = clickableSpan.end
                    if (from < clickableStart) {
                        freeRanges.add(Range(from, clickableStart))
                    }
                    from = clickableEnd
                }
                if (clickableSpans.isNotEmpty()) {
                    val lastUpperBlocker = clickableSpans[clickableSpans.size - 1].end
                    if (lastUpperBlocker < end) {
                        freeRanges.add(Range(lastUpperBlocker, end))
                    }
                }
            } else if (start == end) {
                freeRanges = emptyList()
            } else {
                freeRanges = listOf(Range(start, end))
            }
            return freeRanges
        }

        /**
         * @return a [List] of [Span]s holding {@param type}s, sorted ascending by the span start.
         */
        private fun <T> getSortedSpans(
            builder: SpannableBuilder,
            type: Class<T>,
            start: Int,
            end: Int
        ): List<SpannableBuilder.Span> {
            return builder.getSpans(start, end)
                .stream()
                .filter { span: SpannableBuilder.Span -> type.isInstance(span.what) }
                .sorted(Comparator.comparingInt { o: SpannableBuilder.Span -> o.start })
                .collect(Collectors.toList())
        }
    }
}